在背景執行緒執行一段任務,未完成時就離開 Activity 是有可能造成 Memory leak 的。如下程式碼是一段使用背景執行緒請求網路資料:
networkCall() 模擬 10 秒後取得資料。runOnUiThread 將取得的更新到 UI 上。//未執行完離開App會造成memory leak
object : Thread() {
    override fun run() {
        //背景執行緒取得資料
		val data = networkCall()
		//更新到 UI 上
        runOnUiThread {
            binding.result.text = data
        }
    }
}.start()
//模擬網路請求資料
private fun networkCall(): String {
		sleep(10000)
		return "Data1"
}
上面這段程式碼,如果我們在背景執行緒未執行完成時就離開 Activity,就會因為背景執行緒仍在執行,Activity 無法被釋放,造成 Memory leak。
這個問題將使用 Coroutine 的非同步處理來解決。Coroutine 是在 Kotlin 用來方便處理非同步需求的一個框架。有著易開發、好管理的好處,而且符合結構化並發 (Structured Concurrency) 架構。讓你寫非同步就跟同步一樣的簡單。另外 Coroutine 也是 Android 進行非同步程式設計時,官方的推薦解決方案。
首先修改網路請求資料的 networkCall function。
suspend,使變成 suspending function。withContext(Dispatchers.IO) 切換至背景執行緒//模擬網路請求資料
private suspend fun networkCall(): String {
    val data = withContext(Dispatchers.IO){
			delay(10000)
			"Data1"
		}
	return data
}
接著在 Activity 就可以透過 lifecycleScope.launch 來呼叫 networkCall 取得資料,當執行 networkCall 時會切換至背景執行緒,取得資料後就會再回到 UI 執行緒,這種寫法非常方便,讓寫非同步就跟寫同步一樣。
使用 lifecycleScope.launch 建立一個 Coroutine 來執行 networkCall。
lifecycleScope.launch {
    //取得資料
    val data = networkCall()
    //回到UI執行緒
    binding.result.text = data
}
在lifecycleScope 裡的這段程式碼的生命週期將與 Activity 的生命週期一致,所以當 Activity 被銷毀,Coroutine 執行中的任務也將被取消,也就不會發生當離開 Activity 時,背景執行緒仍在執行。
Coroutine 在處理多個執行緒時,尤其是有階層關系時,比起使用 Thread,更來得容易管理,也能減少發生 Memory leak 的機會,
如下例,我們新增了兩個 job 分別處理網路請求 networkCall 與其子任務 networkCall2。當我們使用 job.join 等待所有的 Coroutine 工作完成時。像這樣的階層關系,使用 Coroutine 就非常方便,會幫你處理好等到所有的子 Coroutine 都完成才算是完成。
job = lifecycleScope.launch {
    try {
        val data = networkCall()
        val job2 = launch {
            val data2 = networkCall2()
        }
    } catch (e: CancellationException) {
        println("Cancel done")
    }
}
lifecycleScope.launch {
    job?.join()
    println("All done")
}
再看另一個範例是 Coroutine 的取消。我們對 job 呼叫了 job.cancelAndJoin 來取消這個 Coroutine 的執行,子 Coroutine 也會被取消。這是 Coroutine 的一個很棒的地方,不用擔心會有子任務在父任務取消後仍在背景執行。
job = lifecycleScope.launch {
    try {
        val data = networkCall()
        //child
        val job2 = launch {
            val data2 = networkCall2()
        }
    } catch (e: CancellationException) {
        println("Cancel done")
    }
}
lifecycleScope.launch {
    job?.cancelAndJoin()
    println("All done")
}
如果父 Coroutine 取消或失敗了,我們不會希望要還有在背地裡執行的執行緒,因為這容易產生 Memory leak。以上舉的這幾個 Coroutine 範例,其實都是在確保當一個任務不再需要被執行,其子任務也都將被取消。
最後,Coroutine 已經是現在開發 Android 一定會使用的,好處是非同步的處理更方便、不易出錯、減少 Memory leak 機會。這邊只是初步的介紹 Coroutine 如何減少 Memory leak,Coroutine 的詳細介紹請見Coroutine官網。
參考:
https://kotlinlang.org/docs/coroutines-guide.html
https://developer.android.com/kotlin/coroutines